/*
 * This file is part of the Boring Engine.
 *
 * Copyright (c) 2011 by pf5234 <pf5234@users.sourceforge.net>.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

package com.googlecode.boringengine.lua;

import com.googlecode.boringengine.GameLoader;
import com.googlecode.boringengine.Log;
import com.googlecode.boringengine.Main;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import org.luaj.compiler.LuaC;
import org.luaj.platform.J2sePlatform;
import org.luaj.vm.LClosure;
import org.luaj.vm.LNil;
import org.luaj.vm.LPrototype;
import org.luaj.vm.LString;
import org.luaj.vm.LTable;
import org.luaj.vm.LUserData;
import org.luaj.vm.LValue;
import org.luaj.vm.LoadState;
import org.luaj.vm.Lua;
import org.luaj.vm.LuaErrorException;
import org.luaj.vm.LuaState;
import org.luaj.vm.Platform;

public class LuaInterpreter {

    private static LuaState state;
    private static HashMap<Object, LValue> objTables = new HashMap<Object, LValue>();
    private static ArrayList<CallLaterInfo> callLaters = new ArrayList<CallLaterInfo>();
    private static LuaObject luaObj;

    public static void init() {
        Platform.setInstance(new J2sePlatform());
        state = Platform.newLuaState();
        LuaC.install();
        state._G.put("GAME_VERSION", LString.valueOf(Main.VERSION));
        state._G.put("state", new LTable());
        luaObj = new LuaObject();
        addObject(luaObj, "lua");
    }

    public static void addObject(Object obj, String name) {
        Log.debug("Adding %s as '%s'", obj, name);
        if (obj == null || name == null || name.equals("")) return;
        Wrapper.load(state._G, obj.getClass(), name, obj);
        if (!name.equals("self"))
            objTables.put(obj, state._G.get(LString.valueOf(name)));
    }

    public static void deleteObject(Object obj) {
        state._G.hashSet(objTables.get(obj), null);
        objTables.remove(obj);
    }

    public static void reset() {
        LValue gamestate = state._G.get(LString.valueOf("state"));
        state = Platform.newLuaState();
        state._G.put("GAME_VERSION", LString.valueOf(Main.VERSION));
        state._G.put("state", gamestate);
        objTables.clear();
        addObject(luaObj, "lua");
    }

    public static void runScript(Object obj, String fileName, GameLoader loader) {
        if (!objTables.containsKey(obj))
            addObject(obj, "self");
        else
            state._G.put("self", objTables.get(obj));
        state._G.put("__loader", new LUserData(loader));
        InputStream in = loader.getFile(fileName);
        LPrototype p;
        try {
            p = LoadState.undump(state, in, fileName);
            LClosure c = p.newClosure(state._G);  // create closure and execute
            state.doCall(c, new LValue[0]); // do the call
        } catch (IOException ex) {
            Log.error("IO Error: %s", ex.getMessage());
        } catch (LuaErrorException ex) {
            Log.error("Lua Error: %s", ex.getMessage());
        }
        state._G.hashSet(LString.valueOf("self"), null);
    }

    public static void update() {
        Iterator<CallLaterInfo> iter = callLaters.iterator();
        while (iter.hasNext()) {
            CallLaterInfo info = iter.next();
            if (info.delay <= 0) {
                if (info.func == null)
                    runScript(info.self, info.fileName, getGameLoader());
                else
                    runFunc(info.self, info.func, info.args);
                iter.remove();
            } else
                info.delay--;
        }
    }

    public static void load(String str) {
        state._G.hashSet(LString.valueOf("state"), null);
        LPrototype p;
        try {
            p = LoadState.undump(state, new ByteArrayInputStream(str.getBytes()), "<string>");
            LClosure c = p.newClosure(state._G);  // create closure and execute
            state.doCall(c, new LValue[0]); // do the call
        } catch (IOException ex) {
            Log.error("IO Error: %s", ex.getMessage());
        } catch (LuaErrorException ex) {
            String mess = ex.getMessage();
            Log.error("Lua loading error: %s", mess);
        }
    }

    public static String save() {
        return "state = " + dumpTable((LTable)state._G.get(LString.valueOf("state"))).replaceAll("\n", "\\\\n");
    }

    private static String dumpTable(LTable table) {
        String str = "{";
        LValue key = LNil.NIL;
        while (table.next(state, key, false)) {
            key = state.topointer(1);
            LValue value = state.topointer(2);
            state.pop(2);
            if (key.isNil()) continue;
            if (key.isInteger())
                str += " [" + key.toJavaString() + "] = ";
            else
                str += " " + key.toJavaString() + " = ";
            if (value.isTable())
                str += dumpTable((LTable)value) + ",";
            else if (value.luaGetType() == Lua.LUA_TSTRING)
                str += "\"" + value.toJavaString() + "\",";
            else
                str += value.toJavaString() + ",";
        }
        state.pop(1);
        str += " }";
        return str;
    }

    public static Object getObject(LValue val) {
        for (Object o : objTables.keySet()) {
            if (objTables.get(o).equals(val))
                return o;
        }
        if (val instanceof LTable) {
            LTable t = (LTable) val;
            return t.get(LString.valueOf("___object")).toJavaInstance();
        }
        return null;
    }

    private static void runFunc(Object obj, LClosure func, LValue[] args) {
        if (!objTables.containsKey(obj))
            addObject(obj, "self");
        else
            state._G.put("self", objTables.get(obj));
        try {
            state.doCall(func, args); // do the call
        } catch (LuaErrorException ex) {
            Log.error("Lua Error: %s", ex.getMessage());
        }
        state._G.hashSet(LString.valueOf("self"), null);
    }

    static Object getSelf() {
        LValue val = state._G.get(LString.valueOf("self"));
        if (val == null) return luaObj;
        for (Object o : objTables.keySet()) {
            if (objTables.get(o).equals(val))
                return o;
        }
        return luaObj;
    }

    static GameLoader getGameLoader() {
        LValue val = state._G.get(LString.valueOf("__loader"));
        if (val == null || !(val instanceof LUserData)) return null;
        return (GameLoader) ((LUserData) val).toJavaInstance();
    }

    static void add(CallLaterInfo info) {
        callLaters.add(info);
    }

    static LValue convertObj(Object obj) {
        if (objTables.containsKey(obj))
            return objTables.get(obj);
        Log.debug("Adding %s...", obj);
        if (obj == null) return LNil.NIL;
        LTable nu = new LTable();
        Wrapper.load(nu, obj.getClass(), null, obj);
        return nu;
    }
}
